package com.mongo.config;

import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.MongoDriverInformation;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import com.mongodb.client.internal.MongoClientImpl;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.Environment;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;

import java.time.*;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * mongo的自动配置,没有配置{@link MongoTransactionManager 时会默认加载此类}
 *
 * @author xiazhun
 * @create 2022/5/24
 */
@Configuration
@ConditionalOnWebApplication
@ConditionalOnMissingBean(MongoTransactionManager.class)
@EnableConfigurationProperties({MongoMapperProperties.class, MongoConfig.MongoClientOptionProperties.class})
public class MongoConfig {
    @Bean
    public MongoDatabase getUseDatabase(MongoDatabaseFactory mongoDatabaseFactory) {
        return mongoDatabaseFactory.getMongoDatabase();
    }

    @Bean
    public MongoCustomConversions mongoCustomConversions() {
        List<Converter<?, ?>> converterList = new ArrayList<>();
        converterList.add(new LocalDateTimeToDateConverter());
        return new MongoCustomConversions(converterList);
    }

    @Bean
    public GridFSBucket gridFSBucket(MongoDatabase mongoDatabase) {
        return GridFSBuckets.create(mongoDatabase);
    }


    @Bean
    public MongoDatabaseFactory mongoDatabaseFactory(MongoClientOptionProperties properties) {
            MongoClientSettings mongoClientOptions = mongoClientSettings(properties);

            MongoDriverInformation info = MongoDriverInformation.builder().build();

            MongoClient mongoClient = new MongoClientImpl(mongoClientOptions, info);

            return new SimpleMongoClientDatabaseFactory(mongoClient, properties.database);
    }


    @Bean
    public MongoClientSettings mongoClientSettings(MongoClientOptionProperties properties) {
        // 创建认证
        MongoCredential mongoCredential = getCredential(properties);

        String address = properties.getAddress();
        List<ServerAddress> serverAddressList;
        if (StringUtils.isNotBlank(address)) {
            serverAddressList = getServerAddress(properties.getAddress());
        } else {
            serverAddressList = new ArrayList<>();
            ServerAddress serverAddress = new ServerAddress(properties.getHost(), properties.getPort());
            serverAddressList.add(serverAddress);
        }

        // 解析获取mongo服务地址


        return MongoClientSettings.builder()
                //#客户端的标识，用于定位请求来源等，一般用程序名
                .applicationName(properties.clientName)
                //配置IP地址
                .applyToClusterSettings(i -> i.hosts(serverAddressList))
                //配置认证
                .credential(mongoCredential)

                .applyToSocketSettings(i -> i
                        //TCP（socket）读取超时时间，毫秒
                        .readTimeout(properties.readTimeoutMs, TimeUnit.MILLISECONDS)
                        //TCP（socket）连接超时时间，毫秒
                        .connectTimeout(properties.connectionTimeoutMs, TimeUnit.MILLISECONDS))

                .applyToConnectionPoolSettings(i -> i
                        //TCP（socket）连接闲置时间，毫秒
                        .maxConnectionIdleTime(properties.maxConnectionIdleTimeMs, TimeUnit.MILLISECONDS)
                        //TCP（socket）连接最多可以使用多久，毫秒
                        .maxConnectionLifeTime(properties.maxConnectionIdleTimeMs, TimeUnit.MILLISECONDS)
                        //当连接池无可用连接时客户端阻塞等待的最大时长，毫秒
                        .maxWaitTime(properties.maxWaitTimeMs, TimeUnit.MILLISECONDS)
                        .maxSize(properties.connectionsPerHost)
                        .minSize(properties.minConnectionsPerHost))

                .applyToServerSettings(i -> i
                        .heartbeatFrequency(properties.heartbeatFrequencyMs, TimeUnit.MILLISECONDS)
                        .minHeartbeatFrequency(properties.minHeartbeatFrequencyMs, TimeUnit.MILLISECONDS))
                .build();
    }


    /**
     * 获取数据库服务地址
     *
     * @param mongoAddress 地址字符串
     * @return 服务地址数组
     */
    private List<ServerAddress> getServerAddress(String mongoAddress) {
        String[] mongoAddressArray = mongoAddress.trim().split(",");
        List<ServerAddress> serverAddressList = new ArrayList<>(4);
        for (String address : mongoAddressArray) {
            String[] hostAndPort = address.split(":");
            serverAddressList.add(new ServerAddress(hostAndPort[0], Integer.parseInt(hostAndPort[1])));
        }
        return serverAddressList;
    }


    /**
     * 创建认证
     *
     * @param properties 属性配置类
     * @return 认证对象
     */
    private MongoCredential getCredential(MongoClientOptionProperties properties) {
        if (!StringUtils.isEmpty(properties.getUsername()) && !StringUtils.isEmpty(properties.getPassword())) {
            String database = StringUtils.isEmpty(properties.getAuthenticationDatabase()) ?
                    properties.getDatabase() : properties.getAuthenticationDatabase();
            return MongoCredential.createCredential(properties.getUsername(), database,
                    properties.getPassword().toCharArray());
        }
        return null;
    }


    @Bean
    @ConditionalOnProperty(prefix = "mongo",name = "open",havingValue = "true")
    public ProxyRegister proxyRegister(Environment environment) {
        String property =
                Optional.ofNullable(environment.getProperty("mongo.mapperPath"))
                        .orElse(environment.getProperty("mongo.mapper-path"));
        String[] properties = null;
        if (null != property){
            properties = property.split(",");
        }
        return new ProxyRegister(properties);
    }

    @Bean
    public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory, MongoCustomConversions mongoCustomConversions, MongoMappingContext mappingContext) {
        DefaultDbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory);
        MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(dbRefResolver, mappingContext);
        mappingMongoConverter.setCustomConversions(mongoCustomConversions);
        mappingMongoConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return new MongoTemplate(mongoDatabaseFactory, mappingMongoConverter);
    }

    @Bean
    public MongoMappingContext mongoMappingContext() {
        return new MongoMappingContext();
    }


    @WritingConverter
    private static class LocalDateTimeToDateConverter implements Converter<LocalDateTime, Date> {
        @Override
        public Date convert(LocalDateTime source) {
            ZoneId zoneId = ZoneId.systemDefault();
            ZonedDateTime zonedDateTime = source.atZone(zoneId);
            Instant instant = zonedDateTime.toInstant();
            return Date.from(instant);
        }
    }

    @Bean
    public GridFsTemplate gridFsTemplate(MongoDatabaseFactory mongoDatabaseFactory) {
        return new GridFsTemplate(mongoDatabaseFactory,
                new MappingMongoConverter(new DefaultDbRefResolver(mongoDatabaseFactory),
                        new MongoMappingContext()));
    }

    @Setter
    @Getter
    @Accessors(chain = true)
    @ConfigurationProperties(prefix = "spring.data.mongodb")
    public class MongoClientOptionProperties {
        /**
         * 基础连接参数
         */
        // 要连接的数据库
        private String database;
        // 用户名
        private String username;
        // 密码
        private String password;
        // 配置集群时的地址
        private String address;
        // 端口
        private int port;
        // ip地址
        private String host;
        // 设置认证数据库，如果有的话
        private String authenticationDatabase;

        /**
         * 客户端连接池参数
         */
        private String clientName; // 客户端的标识，用于定位请求来源等，一般用程序名
        private int connectionTimeoutMs; // TCP（socket）连接超时时间，毫秒
        private int maxConnectionIdleTimeMs; // TCP（socket）连接闲置时间，毫秒
        private int maxConnectionLifeTimeMs; // TCP（socket）连接最多可以使用多久，毫秒
        private int readTimeoutMs; // TCP（socket）读取超时时间，毫秒
        private int maxWaitTimeMs = 1000 * 60 * 2; // 当连接池无可用连接时客户端阻塞等待的最大时长，毫秒
        private int heartbeatFrequencyMs; // 心跳检测发送频率，毫秒
        private int minHeartbeatFrequencyMs; // 最小的心跳检测发送频率，毫秒
        private int heartbeatConnectionTimeoutMs; // 心跳检测连接超时时间，毫秒
        private int heartbeatReadTimeoutMs; // 心跳检测读取超时时间，毫秒
        private int connectionsPerHost = 100; // 线程池允许的最大连接数
        private int minConnectionsPerHost; // 线程池空闲时保持的最小连接数
        // 计算允许多少个线程阻塞等待时的乘数，算法：threadsAllowedToBlockForConnectionMultiplier*maxConnectionsPerHost
        private int threadsAllowedToBlockForConnectionMultiplier;
    }
}
